﻿package com.frontalcode 
{
	import flash.text.*;
	import flash.events.*;
	import flash.geom.*;
	
	/**                  
     * The ContainerText class handles the <text /> tag.
     */
    public class ContainerText extends ContainerInput
    {
		public var textField : TextField;
		
		private var trackVerticalScroll : Boolean = true;
		private var renderedTextWidth : Number;
		private var renderedTextHeight : Number;
		private var shrinkWidth : Boolean = false;
		private var shrinkHeight : Boolean = false;
		private var maxWidth : Number;
		private var lastWidthSpecified : *;
		private var lastTextWidth : Number;
		private var lastTextHeight : Number;
		private var lastText : String;
		private var lastMaxScrollH : int = 0;
		private var lastMaxScrollV : int = 0;
		private var forceLayout : Boolean = true;

		/**
		 * Creates a new ContainerText instance.
		 * 
		 * @param document	The document this element belongs to.
		 * @param parent	The element's parent in the document tree.
		 * @param node		The XML node this element is associated with.
		 * @param options	Options affecting the behavior of this instance.
		 * 					(This object does not have built-in support for any
		 * 					options.)
		 */
		public function ContainerText ( document : Document, parent : DocumentElement, node : XML, options : Object = null )
		{
			super ( document, parent, node, options );
			
			processesOwnContent = true;
		}

		/**
		 * @private
		 */
		override public function onFirstDocumentPreRender ( ) : void
		{
			super.onFirstDocumentPreRender ( );
			if ( textField == null ) 
			{
				input = textField = new TextField ( );
				
				// If the text is an input then initialize it one-time with our
				// tag contents.
				//
				if ( getStyle ( "flash-text-type" ) == "input" ) textField.text = text;				
				
				movie.addChild ( textField );
			}
		}
		
		/**
		 * @private
		 */
		override public function customRender ( ) : void
		{
			super.customRender ( );
			
			if ( forceLayout || attributes.styles.styleCategoryHasChanges ( "textFormat" ) ) styleTextField ( false );
			
			// If a width is set on this text tag, then we use that as a
			// constraint on the width of the TextField. We actually use the
			// contentWidth which will remove space used by scrollbars.
			//
			// If no width has been set on this text tag then we set the  
			// TextField width based on 100% of our leftover. Then if the final
			// result is smaller than this, we shrink our width to the actual
			// value. We're careful in this scenario to maintain any current
			// content width as it isn't included in the leftover calculation
			// and we don't want to lose it.
			//
			// Now there is a special scenario in this case which is that the
			// container's leftover calculation is affected by margins, padding 
			// or borders that have an "auto" value. The problem in this case is
			// that those values were calculated based on the text having zero
			// dimensions and so they've already used up all the leftover space!
			// There's nothing left for the text! So in this scenarios, we steal 
			// back the auto-width dimensions.
			//
			// If the width is auto then we do not use the above calculations
			// but rather just layout the text as entered and see what its
			// dimensions are. The reason for this is that "100% leftover" makes
			// some big assumptions like that the text is for some reason 
			// supposed to fit in the parent. If the parent is scrollable then
			// that's obviosuly not the case. This behavior was added on March 1
			// of 2010 for Roman Thomas and so has surely broken some sites.
			//
			// If a height is set on the text tag, then the height of the 
			// TextField is set. This will potentially create a scrollable
			// TextField.
			//
			// XXX If a height is set on this text tag and no width is set then 
			// we should try to calculate the minimum width that doesn't cause
			// this height to be exceeded. We don't do this though because of
			// the iterative and inefficient nature of this process.
			//
			
			var restoreTextProperties : Boolean  = false;
			var saveWordWrap : Boolean  = textField.wordWrap;
			var textWidth : Number;
			var textHeight : Number;
			
			// XXX In case our styles change, we want to be able to go from 
			// shrinking to not shrinking but this cause 10 renders if we reset
			// to false each time.
			//
			// shrinkWidth  = false;
			// shrinkHeight = false;

			if ( ! nonAutoWidthSpecified )
			{
				if ( getStyle ( "width" ) != "auto" )
				{
					// Here we are trying to come up with some correct width for
					// the text. If the number we come up with is 0 then we 
					// calculate the width as if it were auto.
					//
					var leftover : Number = internalEvaluateDimensionStyle ( "100% leftover", true, NaN, true );
					if ( getStyle ( "margin-left"        ) == "auto" ) leftover += marginLeft;
					if ( getStyle ( "margin-right"       ) == "auto" ) leftover += marginRight;
					if ( getStyle ( "padding-left"       ) == "auto" ) leftover += paddingLeft;
					if ( getStyle ( "padding-right"      ) == "auto" ) leftover += paddingRight;
					if ( getStyle ( "border-left-width"  ) == "auto" ) leftover += borderLeft;
					if ( getStyle ( "border-right-width" ) == "auto" ) leftover += borderRight;
					textWidth = maxWidth = ( isNaN ( contentWidth ) ? 0 : contentWidth ) + leftover;
				}
				
				if ( getStyle ( "width" ) == "auto" || maxWidth == 0 )
				{
					textField.wordWrap = false;
					restoreTextProperties = true;
					textWidth = Number.NaN;
					maxWidth = Number.POSITIVE_INFINITY;
				}
				
				shrinkWidth = true;
			}
			else
			{
				maxWidth = Number.POSITIVE_INFINITY;
				textWidth = contentWidth;
				shrinkWidth = false;
			}
			
			lastWidthSpecified = nonAutoWidthSpecified;
			
			if ( getStyle ( "height" ) == null || getStyle ( "height" ) == "auto" )
			{
				shrinkHeight = true;
			}
			else
			{
				if ( isNaN ( contentHeight ) )
				{
					shrinkHeight = true;
				}
				else
				{
					textHeight = contentHeight;
					shrinkHeight = false;
				}
			}

			var value : String = text;
			
			switch ( getStyle ( "text-transform" ) )
			{
				case "uppercase":
					value = value.toUpperCase ( );
					break;
				case "lowercase":
					value = value.toLowerCase ( );
					break;
				case "capitalize":
					value = Util.capitalize ( value );
					break;
			}
			
			if ( forceLayout || attributes.styles.styleCategoryHasChanges ( "textLayout" ) || textWidth != lastTextWidth || textHeight != lastTextHeight || value != lastText )
			{
				// When setting our TextField width and height to just contain the
				// text, we set our width at 6 pixels too wide and our height at 4
				// pixels too tall because otherwise Flash might reflow the text
				// such that the width and height get changed after we set them.
				// That is, compare the textWidth just after setting the text to 
				// the textWidth just after shrinking and you will se up to 6 pixels 
				// difference. I can't explain why though perhaps it's the 2 pixel 
				// gutter on either side plus a potential 1 pixel on either side for 
				// antialiasing? In the height direction, it seems to just be the 
				// gutter coming into play.
				//
				// But this is why this stuff gets so complicated and why we need to
				// save the value of the textWidth and textHeight just after 
				// rendering. That is, if we shrink the TextField then we need to
				// return an intrinsic width and height based on those saved values
				// rather than the current textWidth and textHeight because those
				// changed after the shrink.
				//
				
				lastTextWidth = textWidth;
				if ( ! isNaN ( textWidth ) ) textField.width = textWidth;
				
				lastTextHeight = textHeight;
				if ( ! isNaN ( textHeight ) ) textField.height = textHeight;
				
				if ( lastText != value || ( attributes.styles.styleCategoryHasChanges ( "textFormat" ) && textIsFormatted ) )
				{
					if ( textField.type != TextFieldType.INPUT ) textField [ getStyle ( "html-text" ) ? "htmlText" : "text" ] = value;
					lastText = value;
				}
				
				renderedTextWidth	= textField.textWidth;
				renderedTextHeight	= textField.textHeight;
	
				if ( shrinkWidth  ) textField.width  = Math.min ( maxWidth, Math.ceil ( renderedTextWidth  ) + 6 );
				if ( shrinkHeight ) textField.height = Math.ceil ( renderedTextHeight ) + 4;
				
				if ( textField.maxScrollV > 0 || textField.maxScrollH > 0 )
				{
					addListener ( textField, Event.SCROLL, textFieldScrollHandler );
				}
				else
				{
					removeListener ( textField, Event.SCROLL, textFieldScrollHandler );
				}
			}
			else if ( attributes.styles.styleCategoryHasChanges ( "textFormat" ) && textField.type != TextFieldType.INPUT && textIsFormatted ) 
			{
				textField.htmlText = value;
			}	
			
			if ( restoreTextProperties ) textField.wordWrap = saveWordWrap;
			
			attributes.styles.clearChanges ( "textFormat" );
			attributes.styles.clearChanges ( "textLayout" );
			forceLayout = false;
		}
		
		internal function get textIsFormatted ( ) : Boolean
		{
			// If we are using HTML text and we see any formatting tags in our
			// text then we say the text is formatted.
			//
			var tagMatch : RegExp = /<\s*(b|i|font|p|span|textformat|u)\s[^>]*>/i;
			return getStyle ( "html-text" ) && tagMatch.test ( text );
		}
		
		/**
		 * @private
		 */
		public function textFieldScrollHandler ( e : Event ) : void
		{
			if ( trackVerticalScroll && textField.maxScrollV > 1 ) super.scrollTop  = scrollTopMax  * ( ( textField.scrollV - 1 ) / ( textField.maxScrollV - 1 ) );
			if ( textField.maxScrollH > 1 ) super.scrollLeft = scrollLeftMax * ( ( textField.scrollH - 1 ) / ( textField.maxScrollH - 1 ) );
		}
		
		/**
		 * @private
		 */
		override public function applyMovieStyles ( ) : void
		{
			super.applyMovieStyles ( );
			textField.mouseEnabled = getStyle ( "mouse-enabled" ) !== undefined ? getStyle ( "mouse-enabled" ) : true;
		}
		
		/**
		 * @private
		 */
		override public function set scrollTop ( value : Number ) : void
		{
			super.scrollTop = value;
			if ( textField != null ) 
			{
				// A TextField's vertical scroll is in lines.
				//
				var targetScroll : uint = Math.floor ( ( scrollTop / scrollTopMax ) * ( textField.maxScrollV - 1 ) + 1 );
				if ( targetScroll != textField.scrollV )
				{
					trackVerticalScroll = false;
					textField.scrollV = targetScroll;
					trackVerticalScroll = true;
				}
			}
		}
		
		/*
			Function: getScrollByDuration
			
				We return 0 if a scrollBy request of less than a single line is
				being made since we can't animate a single line of srolling in a
				TextField. This will prevent scrollBy in Container from doing
				any tweening of the scroll. If more than a single line is being
				requested then return the base class's value.
				
			Parameters:
			
				delta	- The requested amount to scroll. (The actual amount may
						  be less if delta extends the scroll past its valid
						  range.)
				
			Returns:
				
				The duration for the scrollBy tween. Return 0 if no tweening
				should be done.
		*/
		/**
		 * @private
		 */
		override public function getScrollByDuration ( delta : Number ) : Number 
		{ 
			var deltaLines : int = Math.abs ( textField.scrollV - Math.floor ( ( ( scrollTop + delta ) / scrollTopMax ) * ( textField.maxScrollV - 1 ) + 1 ) );
			return deltaLines <= 1 ? 0 : super.getScrollByDuration ( delta );
		}
		
		/*
			Functions: getScrollByButtonPress, getScrollByButtonClick
			
				These functions use the styles "scroll-text-by-button-press" and
				"scroll-text-by-button-click" respectively along with the
				current scroll state and whether the request is to scroll up or
				down to determine the target line. The target line is then
				converted into a pixel amount compatible with the scrolling
				mechanism in Container.
				
			Parameters:
			
				positive	- Is the direction of the scroll positive or 
							  negative?
							  
			Returns:
			
				The amount in pixels that the scrollbar should scroll under each
				event. This amount is always positive. (The base class will 
				negate the returned value if necessary.)
		*/
		/**
		 * @private
		 */
		override public function getScrollByButtonPress ( positive : Boolean, isVertical : Boolean = true ) : * 
		{ 
			if ( isVertical )
			{
				// Return an amount that puts us in the middle of the target line.
				//
				var targetLine : Number = textField.scrollV + getStyle ( "scroll-text-by-button-press" ) * ( positive ? 1 : -1 );
				return Math.abs ( ( targetLine - 0.5 ) * scrollTopMax / ( textField.maxScrollV - 1 ) - scrollTop );
			}
			else
			{
				return getStyle ( "scroll-text-by-button-press" ) * ( positive ? 1 : -1 );
			}
		}
		
		/**
		 * @private
		 */
		override public function getScrollByButtonClick ( positive : Boolean, isVertical : Boolean = true ) : * 
		{
			if ( isVertical )
			{
				// Return an amount that puts us in the middle of the target line.
				//
				var targetLine : Number = textField.scrollV + getStyle ( "scroll-text-by-button-click" ) * ( positive ? 1 : -1 );
				return Math.abs ( ( targetLine - 0.5 ) * scrollTopMax / ( textField.maxScrollV - 1 ) - scrollTop );
			}
			else
			{
				return getStyle ( "scroll-text-by-button-click" ) * ( positive ? 1 : -1 );
			}
		}
		
		/**
		 * @private
		 */
		override public function set scrollLeft ( value : Number ) : void
		{
			super.scrollLeft = value;
			if ( textField != null ) 
			{
				if ( textField.maxScrollH > 1 )
				{
					textField.scrollH = 1 + scrollLeft * ( textField.maxScrollH - 1 ) / scrollLeftMax;
				}
				else
				{
					textField.scrollH = 0;
				}
			}
		}
		
		/*
			Function: layoutChildren
			
				We override layoutChildren because in some sense our TextField
				instance is a child of the text element itself. This is most
				obvious in the case that there are scrollbars positioned at the
				left or top. In that case, we need to shift the TextField.
				
			Parameters:
			
				refreshCache	- The method will caches its results. Set this
								  flag to true to refresh the cache.
								  
			Returns:
			
				An array of Rectangle objects that represent the positions and
				dimensions of the laid-out chldren. 
		*/
		/**
		 * @private
		 */
		override public function layoutChildren ( refreshCache : Boolean = false ) : Array
		{
			var results : Array = super.layoutChildren ( refreshCache );
			
			textField.x = getStyle ( "scrollbar-placement" ).indexOf ( "left" ) >= 0 ? scrollbarWidth  : 0;
			textField.y = getStyle ( "scrollbar-placement" ).indexOf ( "top"  ) >= 0 ? scrollbarHeight : 0;
			
			return results;
		}
		
		/**
		 * @private
		 */
		override public function get intrinsicWidth ( ) : Number 
		{
			var value : Number;
			
			// So the issue here is that a text tag has no width and so sized
			// itself according to those rules. Then, the style is changed but
			// intrinsicWidth is still returning the value previously 
			// calculated and so layout issues occur. For example, if the text 
			// tag were not floating left and then suddenly was with a width of 
			// 100% leftover, unless the intrinsic width were invalidated, it
			// would never achieve a correct layout. The question is, is this a
			// good enough test to invalidate our intrinsic width? Intuitively
			// no but perhaps we're catching those cases elsewhere? (What an 
			// admission.)
			//
			if ( lastWidthSpecified === null || lastWidthSpecified != nonAutoWidthSpecified ) return value;
			
			// Text fields have 2 pixel gutters so we add 4 to the textWidth
			// and textHeight to get our movie width and height.
			//
			// XXX But for some reason, 4 isn't enough on the width and will
			// occasionally cause the text to wrap so we use 6 instead.
			//
			// We don't return the TextField's width and height but rather the
			// textWidth and textHeight plus the margins discussed above. This
			// is because in the case that there should be scrollbars, the
			// textWidth and textHeight will be greater than the width and 
			// height. 
			if ( textField != null && ! isNaN ( renderedTextWidth ) ) value = shrinkWidth ? Math.min ( maxWidth, Math.ceil ( renderedTextWidth  ) + 6 ) : textField.textWidth;
			return value;
		}
		
		/**
		 * @private
		 */
		override public function get intrinsicHeight ( ) : Number 
		{ 
			var value : Number;
			
			// Yes, this is a check on the width when getting the height. See
			// "get intrinsicWidth" for more details.
			//
			if ( lastWidthSpecified === null || lastWidthSpecified != nonAutoWidthSpecified ) return value;
			if ( textField != null && ! isNaN ( renderedTextHeight ) ) value = shrinkHeight ? Math.ceil ( renderedTextHeight ) + 4 : textField.textHeight;
			return value;
		}
		
		public function set text ( value : * ) : void
		{
			if ( ! ( value is XML || value is XMLList ) )
			{
				// Escape any CDATA end tags in our text value.
				//
				var chunks : Array = String ( value ).split ( "]]>" );
				value = null;
				for each ( var chunk : String in chunks ) 
				{
					var tmp : XML = new XML ( "<![CDATA[" + chunk + "]]>" );
					value = value == null ? tmp : value + tmp;
				}
			}
			
			if ( value.toString ( ) == text ) return;
			
			if ( textField != null && textField.type == TextFieldType.INPUT ) 
			{
				textField.text = value.toString ( );
				return;
			}
			
			var texts : XMLList = node.text ( );
			while ( texts.length ( ) > 0 )
			{
				delete texts [ 0 ];
				texts = node.text ( );
			}
			
			node.text ( ) [ 0 ] = value;
			needsRender = true;
		}
		
		public function get text ( ) : String
		{
			if ( textField != null && textField.type == TextFieldType.INPUT ) return textField.text;
			
			return node.text ( ).toString ( );
		}
		
		/**
		 * @private
		 */
		override public function get value ( ) : * { return text; }
		
		/*
			Function: styleTextField
			
				Style a text field based on our styles.
		*/
		/**
		 * @private
		 */
		internal function styleTextField ( resetText : Boolean = true ) : void
		{
			textField.styleSheet = null;
			
			// Set styles based on text properties.
			//
			for each ( var style : String in StylesManager.textPropertyStyles )
			{
				var styleDefn : Object = document.stylesManager.getStyleDefn ( style );
				textField [ styleDefn.options.textProperty ] = getStyle ( style );
			}
						
			var textFormat : TextFormat = new TextFormat ( );
			
			// Set styles based on text properties.
			//
			for each ( style in StylesManager.textFormatPropertyStyles )
			{
				styleDefn = document.stylesManager.getStyleDefn ( style );
				var value : * = getStyle ( style );
				
				// Support percentages for font-size. XXX Why not support 
				// leftover as well? Would need to figure out how it affects the
				// rendering process.
				//
				if ( style == "font-size" && value != null ) value = internalEvaluateDimensionStyle ( value, false, NaN, false );
				textFormat [ styleDefn.options.textFormatProperty ] = value;
			}

			var font : Object = resolveFont ( getStyle ( "font-family" ) );
			textFormat.font = font.name;
			textField.embedFonts = font.isEmbedded;
			
			textField.defaultTextFormat = textFormat;
			textField.setTextFormat ( textFormat );

			// Style sheets break text inputs.
			//			
			if ( textField.type != TextFieldType.INPUT )
			{
				// Process the styles that use Flash stylesheets: 
				// 
				//   link-color
				//   link-decoration
				//   hover-color
				//   hover-decoration
				//
				
				var flashStyleGroups : Object = { link: 'a', hover: 'a:hover' };
				var flashStyles : Object = { color: 'color', decoration: 'textDecoration' };
				var stylesheet : StyleSheet = null;
				
				for ( var group : String in flashStyleGroups )
				{
					var theseStyles : Object = null;
				
					for ( style in flashStyles )
					{
						value = getStyle ( group + '-' + style );
						
						if ( value != null )
						{
							if ( style == 'color' )
							{
								value = Number ( value ).toString ( 16 );
								value = "#" + "000000".substr ( 0, 6 - value.length ) + value;
							}
							
							if ( theseStyles == null ) theseStyles = { };
							theseStyles [ flashStyles [ style ] ] = value;
						}
					}
					
					if ( theseStyles != null )
					{
						if ( stylesheet == null ) stylesheet = new StyleSheet ( );
						stylesheet.setStyle ( flashStyleGroups [ group ], theseStyles );
					}
				}
				
				if ( stylesheet != null ) textField.styleSheet = stylesheet;
			}
			
			if ( textField.type == TextFieldType.INPUT )
			{
				setPseudoClassState ( "isInput", true );
				addListener ( textField, Event.CHANGE, textFieldChangeHandler );
			}
			else
			{
				setPseudoClassState ( "isInput", false );
				removeListener ( textField, Event.CHANGE, textFieldChangeHandler );
				if ( resetText && textIsFormatted ) textField.htmlText = lastText;
			}
		}
		
		override public function onFontResolved ( fontName : String, resolvedFont : Font ) : void 
		{ 
			forceLayout = true;
			super.onFontResolved ( fontName, resolvedFont );
		}

		override internal function resizeHandler ( event : Event ) : void 
		{ 
			super.resizeHandler ( event );
			
			// If our font size is percentage based then a resize means we need
			// to re-style and re-layout ourselves.
			//
			var fontSize : * = getStyle ( "font-size" );
			if ( fontSize != null && fontSize.toString ( ).indexOf ( "%" ) > 0 ) forceLayout = true;
		}
		
		/**
		 * @private
		 */
		public function textFieldChangeHandler ( e : Event ) : void
		{
			if ( textField.maxScrollV != lastMaxScrollV || textField.maxScrollH != lastMaxScrollH ) needsRender = true;
			lastMaxScrollV = textField.maxScrollV;
			lastMaxScrollH = textField.maxScrollH;
		}
		
		/*
			Function: getMappedStyleValue
			
				In cases where the style maps to a TextField or TextFormat, then 
				we can get a current value for it.
				
			Parameters:
			
				style	- The style of interest.
				
			Returns:
			
				The mapped value of the style.
		*/
		/**
		 * @private
		 */
		override public function getMappedStyleValue ( style : String ) : *
		{
			if ( textField != null )
			{
				var styleDefn : Object = document.stylesManager.getStyleDefn ( style );
				if ( styleDefn.options.textProperty       != null ) return textField [ styleDefn.options.textProperty ];
				if ( styleDefn.options.textFormatProperty != null ) return textField.defaultTextFormat [ styleDefn.options.textFormatProperty ];
			}
			
			return super.getMappedStyleValue ( style );
		}
	}
}
